package NG::Sys::Cached::Reference;
use strict;
use warnings;
use utf8;

use JSON::XS;

use NG::Sys::Cached::Common;
our @ISA = qw(NG::Sys::Cached::Common::Parent);

sub init {
	my $self = shift;
	$self->SUPER::init;
	unless($self->{pkey}){
		die "need pkey\n";
	}
	unless($self->{fields}){
		die "need fields\n";
	}
	@{$self->{hash_fields}}{@{$self->{fields}}} = (1) x @{$self->{fields}};
	if($self->{blobs}){
		$self->{hash_blobs} = { map {$_=>1} $self->check_fields_ary($self->{blobs}) };
	}
	$self->set_index($self->{index});
	$self->{row_class} ||= 'NG::Sys::Cached::Reference::Row';
	$self->{cur} = 0;
}
sub set_index {
	my $self = shift;
	my $groups = shift;

	$self->{list_index} = [];
	$self->{hash_index} = {};
	$self->{key_index} = {};

	if($groups){
		my %groups;
		my %groups_s;
		unless(ref $groups){
			$groups = [ $groups ];
		}
		for my $group(@$groups){
			unless(ref $group){
				$group = [$group];
			}
			for my $f(@$group){
				for(split /\./, $f){
					$self->check_field($_);
				}
			}
			my $key = join(":", @$group);
			$groups{$key} = {};
			my $key_s = join(":", sort @$group);
			$groups_s{$key_s} = [$key,$group];
		}
		$self->{list_index} = $groups;
		$self->{hash_index} = \%groups;
		$self->{key_index} = \%groups_s;
		if($self->{list}){
			$self->_prepare;
		}
	}
}
sub add_indexes {
	my $self = shift;
	my $groups = shift;
	if($groups && @$groups){
		$self->set_index([@{$self->{list_index}},@$groups]);
	}
}
sub check_field {
	my $self = shift;
	my $key = shift;
	return exists $self->{hash_fields}{$key} || die "unexists key'".$key."' checked\n";
}
sub check_fields_ary {
	my ($self,$keys) = @_;
	my $h = $self->{hash_fields};
	exists $h->{$_} || die "unexists key'".$_."' checked\n" for @$keys;
	return @$keys;
}
sub renew {
	my $self = shift;
	my $now = time();
	$self->context->{has} = 0;
	$self->{cache}->set($self->{namespace},$now);
	return $now;
}
sub last_new {
	my $self = shift;
	return $self->{cache}->get($self->{namespace});
}
sub _read_data {
	my $self = shift;
	my $force = shift;
	if($force) {
		$self->count_action("force");
		$self->{list} = $self->_read_source;
		$self->_prepare;
		$self->context->{has} = 1;
		return;
	}
	return if $self->context->{has};
	my $cur = ($self->{cur} ||= 0);
	if(my $act = $self->{cache}->get($self->{namespace})){
		if($cur < $act){
			$self->count_action("read");
			$self->{list} = $self->_read_source;
			$self->_prepare;
			$self->{cur} = $act;
		}
	}elsif($cur){
		$self->{cache}->set($self->{namespace},$cur);
	}else{
		$act = time();
		$self->count_action("read");
		$self->{list} = $self->_read_source;
		$self->_prepare;
		$self->{cur} = $act;
		$self->{cache}->set($self->{namespace},$act);
	}
	$self->context->{has} = 1;
}
sub _prepare {
	my $self = shift;
	my %data;
	my $groups = $self->{hash_index};
	for my $key (keys %$groups){
		$groups->{$key} = {};
	}
	my $pkey = $self->{pkey};
	for my $row(@{$self->{list}}){
		if($self->{blobs}){
			for(@{$self->{blobs}}){
				$row->{$_} = decode_json($row->{$_}) if defined $row->{$_};
			}
		}
		my $pk = $row->{$pkey};
		tie my %tmp,
				$self->{row_class},
				$row,
				$self,
				$self->{app}->get_redis,
				$self->{app}->local_block;
		$row = \%tmp;
		$data{$pk} = $row;

		for my $group (@{$self->{list_index}}){
			my $key = join(":", @$group);
			my $to = \$groups->{$key};
			for my $f(@$group){
				my $val = $row;
				for(split /\./, $f){
					$val = $val->{$_};
				}
				$val = '' unless defined $val;
				$to = \$$to->{$val};
			}
			if($$to){
				push @$$to, $row;
			}else{
				$$to = [ $row ];
			}
		}
	}
	return $self->{data} = \%data;
}
sub get_by_key {
	my $self = shift;
	my $key = shift;
	unless(defined $key){
		die "need key";
	}
	$self->_read_data;
	return $self->{data}{$key};
}
sub get_by_index {
	my $self = shift;
	my $index = shift;
	$self->_read_data;

	my $key = $self->{list_index}[0][0];
	my $list = $self->{hash_index}{$key}{$index};
	return $list && $list->[0];
}

sub get_list {
	my $self = shift;
	my $filter = shift;
	$self->_read_data();
	unless($filter and scalar %$filter){
		return $self->{list};
	}
	my $key = join ":", sort keys %$filter;
	my $fk = $self->{key_index}{$key};
	unless($fk){
		die "bad filter '$key'";
	}
	my $list = $self->{hash_index}{$fk->[0]};
	for my $f(@{$fk->[1]}){
		unless($list = $list->{$filter->{$f}}){
			return [];
		}
	}
	return $list;
}

sub get_keys {
	my $self = shift;
	my $field = shift;
	my $filter = shift;
	$filter ||= {};
	$self->_read_data();
	my $key = join ":", sort( ($field,keys %$filter));
	my $fk = $self->{key_index}{$key};
	unless($fk){
		die "bad filter '$key'";
	}
	my $list = $self->{hash_index}{$fk->[0]};
	for my $f(@{$fk->[1]}){
		if($field eq $f){
			return [keys %$list];
		}
		unless($list = $list->{$filter->{$f}}){
			return [];
		}
	}
	die "it's impossible";
}

package NG::Sys::Cached::Reference::Row;
use strict;
use warnings;
use utf8;
use JSON::XS;

sub TIEHASH{
	my $class = shift;
	my $row = shift;
	my $ref = shift;
	my $redis = shift;
	my $locker = shift;

	return bless {
			row => $row,
			ref => $ref->{namespace},
			pkey => $ref->{pkey}, 
			entry => $row->{$ref->{pkey}},
			fields => $ref->{fields},
			blobs => $ref->{hash_blobs}||{},
			extra => $ref->{hash_extra}||{},
			redis => $redis,
			locker => $locker,
		}, $class;
} 
sub EXISTS {
	my $self = shift;
	my $key = shift;
	if($key eq "_"){
		return 1;
	}
	if($self->{extra}{$key}){
		return 1;
	}elsif(exists $self->{row}{$key}){
		return 1;
	}else{
		return 0;
	}
}
sub FETCH {
	my $self = shift;
	my $key = shift;
	if($key eq "_"){
		return $self;
	}
	if($self->{extra}{$key}){
		my $val = $self->{redis}->get($self->{ref}.":".$self->{entry}.":".$key);
		if(defined $val and $self->{blobs}{$key}){
			$val = decode_json($val);
		}
		return $val;
	}elsif(!exists $self->{row}{$key}){
		die "no fields '$key' for reference '$self->{ref}'";
	}else{
		#NG::Sys::App::app()->logger->info("FETCH: ".$self->{ref}.":".$self->{entry}.":".$key);
		return $self->{row}{$key};
	}
}
sub STORE {
	my $self = shift;
	my $key = shift;
	my $val = shift;
	if($self->{extra}{$key}){
		if($self->{blobs}{$key}){
			$val = encode_json($val);
		}
		return $self->{redis}->set($self->{ref}.":".$self->{entry}.":".$key,$val);
	}else{
		die "cannot store '$key' for '$self->{ref}'"; 
	}
}
sub SCALAR {
	my $self = shift;
	return scalar %{$self->{row}};
}
sub FIRSTKEY {
	my $self = shift;
	$self->{pos} = -1;
	return $self->{pkey};
}
sub NEXTKEY {
	my $self = shift;
	unless(defined $self->{pos}){
		return undef;
	}elsif(++$self->{pos} < @{$self->{fields}}){
		return $self->{fields}[$self->{pos}];
	}else{
		return undef;
	}
}
sub get_and_set {
	my $self = shift;
	my $key = shift;
	my $sub = shift;

	unless($self->{extra}{$key}){
		die "cannot doit with it '$self->{ref}' '$key'";
	}
	my $rk = $self->{ref}.":".$self->{entry}.":".$key;
	$self->{redis}->watch($rk);
	my $val = $self->{redis}->get($rk);

	if(defined $val and $self->{blobs}{$key}){
		$val = decode_json($val);
	}

	my $new_val = $sub->($key,$val);

	if(defined $new_val and $self->{blobs}{$key}){
		$new_val = encode_json($new_val);
	}

	$self->{redis}->multi;
	$self->{redis}->set($rk,$new_val);
	$self->{redis}->exec;
}
sub change_by {
	my $self = shift;
	my $key = shift;
	my $by = shift;

	unless($self->{extra}{$key}){
		die "cannot doit with it '$self->{ref}' '$key'";
	}
	my $rk = $self->{ref}.":".$self->{entry}.":".$key;
	if($by < 0){
		return $self->{redis}->decrby($rk,-$by);
	}elsif($by > 0){
		return $self->{redis}->incrby($rk,$by);
	}else{
		return 0;
	}
}
sub lock_get {
	my $self = shift;
	my $key = $self->{ref}.":".$self->{entry};
	return $self->{locker}->get($key);
}
sub lock_free {
	my $self = shift;
	my $key = $self->{ref}.":".$self->{entry};
	return $self->{locker}->free($key);
}
sub lock_try {
	my $self = shift;
	my $key = $self->{ref}.":".$self->{entry};
	return $self->{locker}->try($key);
}

1;
